Host the Merge-Bot in the Hub and Measure Fleet Workflow Reuse - #744
Conversation
The fleet copies its standard workflows into every repo, and a fix to a shared job is a fleet sweep rather than one edit. This lands the design for hosting each standard workflow once in the hub as a workflow_call task, with a downstream caller stub and a composite-action hook for what is genuinely repo-specific, and ships the first task, the merge-bot. - docs/reusable-workflows.md states the target model, the hook contract, the pin and secrets policy, the phases, and the merge-bot adoption. - spec/workflow_reuse.py measures the fleet against that model and writes reports/workflow-reuse.md, the burn-down: 108 files and 10,964 lines downstream today, 49 percent byte-identical to a hub canonical. - merge-bot-task.yml hosts the three jobs with the extra bot pairs as a rules input, declares no GITHUB_TOKEN scope (#521's hub half), and the hub's own merge-bot-pull-request.yml is the first caller stub. - The manifest contract for merge-bot-pull-request.yml is now the caller job, the hub task token, and the two mapped secrets, so every copied merge-bot reports the adoption it owes. - TODO.md carries the phased cluster and the adoption sweep, and the two skills that describe copied orchestration point at the design.
There was a problem hiding this comment.
Pull request overview
This PR advances the fleet’s “reach, don’t carry” workflow strategy by hosting the merge-bot as a hub workflow_call task, adding a measurement tool/report to quantify workflow duplication across the fleet, and updating the audit/spec/docs to treat downstream workflows as thin caller stubs plus hooks.
Changes:
- Introduces a hub-hosted reusable workflow for the merge-bot and converts the hub’s merge-bot entry workflow into a thin caller stub.
- Adds
spec/workflow_reuse.pyplus a generatedreports/workflow-reuse.mdto measure workflow reuse/variants across cataloged repos, and wires the tool’s selftest into CI validation. - Updates audit contracts, documentation, TODO tracking, and skills text to reflect the new reusable-workflow model and merge-bot caller contract.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| WORKFLOW.md | Updates merge-bot/upstream-version tracker contract language to account for caller-provided rules. |
| TODO.md | Reframes and expands the “Hub-Hosted Reusable Workflows” work breakdown and adoption tracking. |
| spec/workflow_reuse.py | Adds workflow-reuse measurement tool + selftest/report rendering. |
| spec/files.json | Updates merge-bot workflow interface contract to require a merge-bot caller job and mapped secrets. |
| spec/fidelity-model.md | Updates fidelity model text to include bot workflows and reference reusable-workflow design doc. |
| spec/audit.py | Adjusts interface-check messaging and adds selftests for the new merge-bot caller stub contract. |
| scripts/tests/test_repo_gate.py | Adds a repo-gate test ensuring reusable-workflow refs resolve/pin-check correctly. |
| reports/workflow-reuse.md | Adds generated fleet reuse/variant report produced by spec/workflow_reuse.py --report. |
| GOVERNANCE.md | Updates “Hub-Hosted Tooling” guidance to explicitly include reusable workflows alongside actions. |
| docs/reusable-workflows.md | Adds hub-only design doc describing the reusable-workflow migration model, hooks, phases, and merge-bot adoption stub. |
| catalog/snippets/workflows/README.md | Documents presence of hub-hosted reusable tasks alongside orchestrators in the hub. |
| catalog/snippets/workflows/check-upstream-version-task.yml | Updates comments/description to reference merge-bot rules (built-ins or caller-provided). |
| .github/workflows/validate-task.yml | Runs spec/workflow_reuse.py --selftest in the CI self-test suite. |
| .github/workflows/merge-bot-task.yml | Adds hub-hosted merge-bot reusable workflow implementing the merge/disable logic with App token. |
| .github/workflows/merge-bot-pull-request.yml | Replaces inlined merge-bot jobs with a thin caller stub that uses: the hub task and maps secrets; sets permissions: {}. |
| .gitattributes | Pins LF EOL for the new shebang-executed Python tool. |
| .editorconfig | Pins LF EOL for spec/workflow_reuse.py alongside other directly-executed spec tooling. |
| .claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md | Updates workflow CI contract skill text to include the reusable-workflow “reached, not carried” model. |
| .claude-plugin/fleet-skills/skills/operational-vs-release-workflow/references/release-publish-mechanics.md | Updates guidance to reference the release-chain phase of reusable-workflow migration. |
| .claude-plugin/fleet-skills/.source-digest | Updates skill source digest. |
| .agents/skills/workflow-ci-contract/SKILL.md | Mirrors reusable-workflow model update in the agent skill copy. |
| .agents/skills/operational-vs-release-workflow/references/release-publish-mechanics.md | Mirrors reusable-workflow migration reference update in the agent skill copy. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (2)
spec/workflow_reuse.py:223
- The report text says variants are clustered by "pairwise similarity", but
cluster()only compares each candidate to a representative (the first member) when adding it to a cluster. To avoid overstating what the clustering guarantees, the wording here should match the implementation.
w(
f"Downstream copies of each hub canonical. A variant is a cluster of copies at or above {CLUSTER_THRESHOLD} pairwise similarity, so the cluster count is how many distinct shapes of one workflow the fleet runs today. Callers are the copies that already reach the hub rather than carrying the job bodies."
)
spec/workflow_reuse.py:98
- The
cluster()docstring says this is doing "single-link" clustering, but the implementation only compares each new repo to the first member of an existing cluster (members[0]). That’s a different clustering strategy and can produce clusters that are not single-link (and not necessarily pairwise-similar), which makes the description misleading.
This issue also appears on line 221 of the same file.
def cluster(copies):
"""Greedy single-link clusters of {repo: text} at CLUSTER_THRESHOLD, each a sorted repo list.
Greedy rather than exhaustive on purpose: the count of variants is what the report needs, and a stricter
partition would only split a borderline pair into two clusters that read the same to a maintainer.
|
Answering the two suppressed findings, "Suppressed comments (2)", from the round on
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (4)
.github/workflows/merge-bot-task.yml:150
- Same issue as above:
inputs.delete-branchuses dot notation. Use bracket syntax for hyphenated input names so the value resolves correctly.
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
DELETE_BRANCH: ${{ inputs.delete-branch }}
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
.github/workflows/merge-bot-task.yml:165
inputs.app-loginis referenced with dot notation in this job condition as well. Use bracket syntax so the disable-auto-merge job correctly recognizes maintainer pushes to App-authored PRs.
if: >-
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name == github.repository &&
(
github.event.pull_request.user.login == 'dependabot[bot]' ||
github.event.pull_request.user.login == inputs.app-login
) &&
github.actor != github.event.pull_request.user.login
.github/workflows/merge-bot-task.yml:79
inputs.delete-branchis referenced with dot notation (inputs.delete-branch). In GitHub Actions expressions, keys containing-must be accessed with bracket syntax (otherwise-is parsed as subtraction), so this will not resolve correctly and can break the--delete-branchbehavior.
This issue also appears on line 146 of the same file.
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
DELETE_BRANCH: ${{ inputs.delete-branch }}
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
.github/workflows/merge-bot-task.yml:89
inputs.app-loginis referenced with dot notation (inputs.app-login). In GitHub Actions expressions, hyphenated keys must use bracket syntax; otherwise this condition may never match and the App PR auto-merge job won’t run.
This issue also appears on line 158 of the same file.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == inputs.app-login &&
github.event.pull_request.head.repo.full_name == github.repository
|
Answering the four suppressed findings, "Suppressed comments (4)", from the round on
The GitHub Actions contexts reference states the property dereference rule: the property name must start with a letter or |
…leaner Report (#746) Promote `develop` to `main`, carrying two changes: - #744 Host the Merge-Bot in the Hub and Measure Fleet Workflow Reuse: the hub-hosted reusable-workflow design (`docs/reusable-workflows.md`), the fleet workflow-reuse measurement (`spec/workflow_reuse.py`, `reports/workflow-reuse.md`), the first hub task (`.github/workflows/merge-bot-task.yml`) with the hub's own caller stub, the manifest contract change for `merge-bot-pull-request.yml`, and the TODO cluster and adoption sweep. - #739 Mark PhotoCleaner Operational After the Promotion of the b09078e Resync: the PhotoCleaner audit report refresh. The release that follows this promotion is the first tag naming `merge-bot-task.yml`, which is the pin the downstream caller stubs use, so the merge-bot adoption sweep and the catalog snippet both wait on it. A Dependabot pull request against `main` after this merge is the `--merge` half of the live proof of the callee. Refs #521 (hub half shipped in #744, sweep half is the adoption). No issue closes on this promotion.
## What - `docs/reusable-workflows.md` gains a **Rollout** section replacing the prose "Migration Phases": six stages (0 design and merge-bot task, 1 merge-bot adoption, 2 gates, 3 pure functions, 4 release chain and Docker core, 5 type-specific), each with hub, release, and per-repo adoption checkboxes. Items are ticked only with the evidence that closed them (PR, commit, tag, run URL), and the section opens with how a session resumes from it: read it first, take the first unchecked item in an open stage, verify against the tree, work in a worktree, tick in the same PR. Stage 0 is ticked with #744, #746, and release `2.0.338`, apart from the two live-proof items that wait on the next Dependabot PRs. - `catalog/snippets/workflows/merge-bot-pull-request.yml`: the caller stub, now that `2.0.338` on `20616e0` carries `merge-bot-task.yml`. The pin resolves under `repo_gate.py check_sha_pin`. The doc's stub carries the same pin, and the catalog README gains the row. - `TODO.md`: the cluster names the Rollout section as the state tracker and drops the snippet entry it just shipped. ## Why The plan lived as prose and as TODO entries, and nothing in git said which stage was done, so a cold session had no first unchecked item to take. Only git persists. ## Verification prose_lint (CI rule list), actionlint, markdownlint, `repo_gate.py` (the new pin resolved against GitHub), unit tests, audit self-test, build_dist --check.
What
The first step of moving the fleet's standard workflows out of every repo and into the hub, once, as
workflow_calltasks a downstream repo reaches through a pinneduses:rather than carries. Repo-specific behavior lands in a composite-action hook at a conventional path, with a hub default where one makes sense.docs/reusable-workflows.md(hub-only): the target model, the hook contract, the pin and secrets policy, the hook catalog, the Docker family design, the migration phases, the merge-bot adoption stub, what a pilot proves, and the open decisions.spec/workflow_reuse.pyandreports/workflow-reuse.md: the burn-down. It reads every cataloged repo's.github/workflows/from its ground-truth branch, compares each file against the hub canonical after the verbatim engine's normalization, and clusters the copies into variants. First run at hub7c67328: 108 files and 10,964 lines across 20 downstream repos, 5,385 lines (49 percent) byte-identical to a hub canonical, one file reaching the hub. Its--selftestruns in CI beside the audit engine's..github/workflows/merge-bot-task.yml: the merge-bot as a hub task. Three jobs,merge-dependabot,merge-app(built-in codegen and upstream-version pairs plus arulesJSON input for a repo's own tracker), anddisable-auto-merge-on-maintainer-push. Inputsapp-login,rules,delete-branch. Explicitsecrets:declarations, noinherit. NoGITHUB_TOKENscope in the callee andpermissions: {}in the caller, which is the hub half of merge-bot: GITHUB_TOKEN permissions are unused, since every write goes through the App token #521..github/workflows/merge-bot-pull-request.yml: the hub's own caller stub, byte-shaped like the downstream one apart from the./uses.spec/files.json: the merge-bot contract becomes the caller job, the hub task token, and the two mapped secrets. Every downstream copy now reportsmissing required job 'merge-bot', which is the adoption work list the TODO sweep entry carries.TODO.md: the "Hub-Hosted Reusable Workflows" cluster (gates, pure functions, release chain and Docker core, type-specific tasks, the catalog snippet, three open decisions, the Dockerfile shape item), the merge-bot adoption sweep, the superseded investigation entry removed, and "The Merge-Bot Token Grants" folded in.bump-branch-prefixcomment, the two stale "override seam" pointers inspec/fidelity-model.mdandspec/audit.py, and the two skills that describe copied orchestration.Why the catalog snippet is not in this PR
catalog/snippets/workflows/is scanned byrepo_gate.py check_sha_pin, so a caller snippet cannot carry a placeholder SHA, and no released hub commit carriesmerge-bot-task.ymluntil this promotes and releases. The stub's text is in the doc's "Adopting the Merge-Bot" and the TODO cluster carries the follow-up.Verification
python3 spec/workflow_reuse.py --selftestand--report(the report is regenerated from the live fleet).merge-apprule filter run standalone against the four built-in pairs, anha-version-bump/prefix ondevelopand onmain, an empty prefix, and three malformedrulesinputs.python3 spec/audit.py --selftestwith four new interface cases for the stub,python3 -m unittest discover -s scripts/tests(668 tests, one new for an owner-scoped reusable-workflow pin),python3 spec/validate.py,python3 scripts/repo_gate.py,python3 scripts/build_dist.py --check,python3 scripts/prose_lint.pywith the CI rule list, actionlint, markdownlint, cspell, editorconfig-checker, ruff check and format, mypy.python3 spec/audit.py HomeAutomation-Configreports the new merge-bot finding as designed.What this PR cannot prove
The hub's own stub proves the callee mechanics on the first Dependabot PR after this lands on
develop, since underpull_request_targeta feature branch's callee resolves from the base branch. What only a downstream adopter proves is cross-repository resolution of the owner-scopeduses:, Dependabot bumping a# <tag>pin on a reusable workflow, and therulesinput end to end. The doc states these as the pilot's checklist rather than as claims.Refs #521 (hub half done here, the sweep half is the adoption). Settles the workflow half of #735 by design once the phases land, and #585 and #729 land inside the gates phase.